<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      :root { --text: #333; }
      html, body, #map { height: 100%; margin: 0; padding: 0; background: #ffffff; }
      .leaflet-container { background: #ffffff; }
      .leaflet-div-icon { background: transparent; border: none; }
      .gh-label { background: transparent; border: none; pointer-events: none; filter: none; }
      .gh-text {
        color: #444444;
        font-weight: 700;
        font-size: 14px;
        line-height: 1;
        text-shadow: 0 0 2px #ffffff, 0 0 2px #ffffff, 0 0 2px #ffffff;
        font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
      }
      .dark .gh-text {
        color: #dddddd;
        text-shadow: 0 0 2px #000000, 0 0 2px #000000, 0 0 2px #000000;
      }
      .gh-text-selected {
        color: #00C851 !important;
      }
    </style>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
  </head>
  <body>
    <div id="map"></div>

    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <script>
      // Minimal geohash (bounds/encode/adjacent)
      (function () {
        const base32 = "0123456789bcdefghjkmnpqrstuvwxyz";
        function bounds(geohash) {
          let evenBit = true; let latMin = -90, latMax = 90, lonMin = -180, lonMax = 180;
          geohash = geohash.toLowerCase();
          for (let i = 0; i < geohash.length; i++) {
            const idx = base32.indexOf(geohash.charAt(i));
            if (idx == -1) throw new Error("Invalid geohash");
            for (let n = 4; n >= 0; n--) {
              const bitN = (idx >> n) & 1;
              if (evenBit) { const lonMid = (lonMin + lonMax) / 2; if (bitN == 1) lonMin = lonMid; else lonMax = lonMid; }
              else { const latMid = (latMin + latMax) / 2; if (bitN == 1) latMin = latMid; else latMax = latMid; }
              evenBit = !evenBit;
            }
          }
          return { sw: { lat: latMin, lng: lonMin }, ne: { lat: latMax, lng: lonMax } };
        }
        function encode(lat, lon, precision) {
          let idx = 0, bit = 0, evenBit = true, hash = "";
          let latMin = -90, latMax = 90, lonMin = -180, lonMax = 180;
          while (hash.length < precision) {
            if (evenBit) { const lonMid = (lonMin + lonMax) / 2; if (lon >= lonMid) { idx = idx * 2 + 1; lonMin = lonMid; } else { idx = idx * 2; lonMax = lonMid; } }
            else { const latMid = (latMin + latMax) / 2; if (lat >= latMid) { idx = idx * 2 + 1; latMin = latMid; } else { idx = idx * 2; latMax = latMid; } }
            evenBit = !evenBit; if (++bit == 5) { hash += base32.charAt(idx); bit = 0; idx = 0; }
          }
          return hash;
        }
        function adjacent(hash, dir) {
          const neighbour = { n:["p0r21436x8zb9dcf5h7kjnmqesgutwvy","bc01fg45238967deuvhjyznpkmstqrwx"], s:["14365h7k9dcfesgujnmqp0r2twvyx8zb","238967debc01fg45kmstqrwxuvhjyznp"], e:["bc01fg45238967deuvhjyznpkmstqrwx","p0r21436x8zb9dcf5h7kjnmqesgutwvy"], w:["238967debc01fg45kmstqrwxuvhjyznp","14365h7k9dcfesgujnmqp0r2twvyx8zb"] };
          const border = { n:["prxz","bcfguvyz"], s:["028b","0145hjnp"], e:["bcfguvyz","prxz"], w:["0145hjnp","028b"] };
          hash = hash.toLowerCase(); const lastCh = hash.slice(-1); let parent = hash.slice(0, -1); const type = hash.length % 2;
          if (border[dir][type].indexOf(lastCh) != -1 && parent != "") parent = adjacent(parent, dir);
          return parent + base32.charAt(neighbour[dir][type].indexOf(lastCh));
        }
        window.__geohash = { bounds, encode, adjacent };
      })();

      const map = L.map("map", { zoomControl: true, minZoom: 2, maxZoom: 21 }).setView([0, 0], 3);
      L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png", { maxZoom: 21, attribution: "&copy; OpenStreetMap &copy; Carto", opacity: 1.0 }).addTo(map);

      let selectedGeohash = "";
      let gridLayer = L.layerGroup().addTo(map);
      let pinnedPrecision = null;
      let outlineColor = "#00C851";

      function getNeighbors(hash) {
        const neighbors = [];
        // N, S, E, W
        neighbors.push(window.__geohash.adjacent(hash, 'n'));
        neighbors.push(window.__geohash.adjacent(hash, 's'));
        neighbors.push(window.__geohash.adjacent(hash, 'e'));
        neighbors.push(window.__geohash.adjacent(hash, 'w'));
        // Diagonals
        neighbors.push(window.__geohash.adjacent(window.__geohash.adjacent(hash, 'n'), 'e'));
        neighbors.push(window.__geohash.adjacent(window.__geohash.adjacent(hash, 'n'), 'w'));
        neighbors.push(window.__geohash.adjacent(window.__geohash.adjacent(hash, 's'), 'e'));
        neighbors.push(window.__geohash.adjacent(window.__geohash.adjacent(hash, 's'), 'w'));
        return neighbors;
      }

      function pickPrecisionForViewport() {
        const c = map.getCenter();
        const minPx = 80;
        const maxPx = 240;
        let chosen = 1;
        let lastAboveMin = 1;
        for (let p = 1; p <= 12; p++) {
          const gh = window.__geohash.encode(c.lat, c.lng, p);
          const b = window.__geohash.bounds(gh);
          const pSw = map.latLngToLayerPoint([b.sw.lat, b.sw.lng]);
          const pNe = map.latLngToLayerPoint([b.ne.lat, b.ne.lng]);
          const cellPx = Math.min(Math.abs(pNe.x - pSw.x), Math.abs(pSw.y - pNe.y));
          if (cellPx >= minPx && cellPx <= maxPx) { chosen = p; break; }
          if (cellPx >= minPx) { lastAboveMin = p; }
          if (cellPx < minPx) { chosen = lastAboveMin; break; }
          if (p === 12) { chosen = 12; }
        }
        return chosen;
      }

      function notifySelection() {
        if (window.Android && window.Android.onGeohashChanged && selectedGeohash) {
          window.Android.onGeohashChanged(selectedGeohash);
        }
      }

      function zoomForPrecision(p) {
        if (p <= 1) return 1; if (p === 2) return 2; if (p === 3) return 3; if (p === 4) return 4;
        if (p === 5) return 5; if (p === 6) return 7; if (p === 7) return 9; if (p === 8) return 11;
        if (p === 9) return 13; if (p === 10) return 15; if (p === 11) return 17;
        return 18;
      }

      function updateOverlay() {
        gridLayer.clearLayers();
        const c = map.getCenter();
        const usePinned = pinnedPrecision !== null;
        const p = usePinned ? pinnedPrecision : pickPrecisionForViewport();
        selectedGeohash = window.__geohash.encode(c.lat, c.lng, p);
        notifySelection();

        const centerBounds = window.__geohash.bounds(selectedGeohash);
        const centerLon = (centerBounds.sw.lng + centerBounds.ne.lng) / 2;
        const centerLat = (centerBounds.sw.lat + centerBounds.ne.lat) / 2;

        const allHashes = [selectedGeohash, ...getNeighbors(selectedGeohash)];

        const filteredHashes = allHashes.filter(gh => {
            if (!gh) return false;
            try {
                const b = window.__geohash.bounds(gh);
                const lon = (b.sw.lng + b.ne.lng) / 2;
                const lat = (b.sw.lat + b.ne.lat) / 2;
                if (Math.abs(lon - centerLon) > 180) return false; // anti-meridian wrap
                if (Math.abs(lat - centerLat) > 90) return false; // pole wrap
                return true;
            } catch (e) { return false; }
        });

        filteredHashes.forEach(gh => {
            const b = window.__geohash.bounds(gh);
            const sw = [b.sw.lat, b.sw.lng];
            const ne = [b.ne.lat, b.ne.lng];
            const isSelected = (gh === selectedGeohash);

            const rect = L.rectangle([sw, ne], {
                color: isSelected ? outlineColor : '#cccccc',
                weight: isSelected ? 3 : 1,
                fillOpacity: 0.0,
                opacity: 0.9,
                interactive: false
            });
            gridLayer.addLayer(rect);

            const center = [(b.sw.lat + b.ne.lat) / 2, (b.sw.lng + b.ne.lng) / 2];
            const labelClass = isSelected ? 'gh-text gh-text-selected' : 'gh-text';
            const label = L.marker(center, {
                icon: L.divIcon({
                    className: 'gh-label',
                    html: `<span class="${labelClass}">${gh}</span>`
                }),
                interactive: false
            });
            gridLayer.addLayer(label);
        });
      }

      map.on("movestart", () => { pinnedPrecision = null; });
      map.on("zoomstart", () => { pinnedPrecision = null; });
      map.on("moveend", updateOverlay);
      map.on("zoomend", updateOverlay);

      function setCenter(lat, lng) { map.setView([lat, lng], map.getZoom()); }
      function setPrecision(p) {
        const clamped = Math.max(1, Math.min(12, p|0));
        const targetZoom = zoomForPrecision(clamped);
        map.setZoom(targetZoom);
      }
      function focusGeohash(gh) {
        if (!gh || typeof gh !== 'string') return;
        const g = gh.toLowerCase();
        const b = window.__geohash.bounds(g);
        pinnedPrecision = g.length;
        map.fitBounds([[b.sw.lat, b.sw.lng],[b.ne.lat, b.ne.lng]], { animate: false, padding: [8,8] });
        selectedGeohash = g;
      }
      function getGeohash() { return selectedGeohash; }
      
      // Android side will call this with 'dark' or 'light'
      function setMapTheme(theme) {
        document.body.className = theme;
      }

      window.setCenter = setCenter;
      window.setPrecision = setPrecision;
      window.focusGeohash = focusGeohash;
      window.getGeohash = getGeohash;
      window.setMapTheme = setMapTheme;

      function cleanup() {
        try { map.off(); } catch (_) {}
        try { gridLayer.clearLayers(); } catch (_) {}
        try { map.remove(); } catch (_) {}
      }
      window.cleanup = cleanup;

      map.whenReady(updateOverlay);
    </script>
  </body>
</html>